﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

namespace HDViewSL
{
	/// <summary>
	/// Parses HD View XML, Zoomify XML, or KML to produce an HD View image set object.
	/// </summary>
	public class Parser
	{
		public string InputText { get; private set; }

		public string InputFormat { get; protected set; }

		public HDViewImageSet ImageSet { get; protected set; }

		public string ErrorMessage { get; protected set; }

		public bool Parse(string inputText, Uri baseUri, string xpath)
		{
			this.InputText = inputText.Trim();
			this.InputFormat = "Unknown format";
			this.ImageSet = null;
			this.ErrorMessage = null;

			XDocument document = null;
			try
			{
				document = XDocument.Parse(this.InputText);
			}
			catch
			{
				this.ErrorMessage = "File does not contain valid XML.";
				return false;
			}

			return this.Parse(document, baseUri, xpath);
		}

		protected virtual string UnknownFormatErrorMessage
		{
			get { return "File is not in HD View, Zoomify, or KML format."; }
		}

		protected virtual bool ParseAdditionalFormats(XDocument document, Uri baseUri)
		{
			return false;
		}

		protected virtual void ProcessKmlPhotoOverlays(XDocument document, Uri baseUri, XNamespace kmlNamespace, IEnumerable<XElement> photoOverlayElements, XElement photoOverlayElement)
		{
		}

		private bool Parse(XDocument document, Uri baseUri, string xpath)
		{
			// Try to parse the document as an HD View XML file, and if that fails,
			// as a Zoomify XML file, and if that fails, as a KML file.
			bool success = this.ParseHDViewXml(document, baseUri);
			success = success || this.ParseZoomifyXml(document, baseUri);
			success = success || this.ParseKml(document, baseUri, xpath);
			success = success || this.ParseAdditionalFormats(document, baseUri);

			if (success)
			{
				if (!Uri.IsWellFormedUriString(this.ImageSet.UriPattern, UriKind.Absolute))
				{
					Uri absoluteUri;
					if (Uri.TryCreate(baseUri, this.ImageSet.UriPattern, out absoluteUri))
					{
						this.ImageSet.UriPattern = absoluteUri.OriginalString;
					}
					else
					{
						this.ErrorMessage = "The tile URL pattern is not valid.";
						success = false;
					}
				}
			}
			else if (this.ErrorMessage == null)
			{
				this.ErrorMessage = this.UnknownFormatErrorMessage;
			}
			return success;
		}

		private bool ParseHDViewXml(XDocument document, Uri baseUri)
		{
			try
			{
				// Find the first imageset element in the XML document.
				XElement imageSetElement = document.Descendants("imageset").FirstOrDefault();
				if (imageSetElement != null)
				{
					// Initialize the image set.
					this.InputFormat = "HD View XML";
					HDViewImageSet imageSet = new HDViewImageSet();
					imageSet.BaseUri = baseUri;
					imageSet.UriPattern = imageSetElement.Attribute("url").Value;
					imageSet.Levels = ParseInt(imageSetElement, XmlNodeType.Attribute, "levels", 0);
					imageSet.Width = ParseInt(imageSetElement, XmlNodeType.Attribute, "width", 0);
					imageSet.Height = ParseInt(imageSetElement, XmlNodeType.Attribute, "height", 0);
					imageSet.Projection = ParseEnumAttribute<HDViewProjection>(imageSetElement, "projection");
					imageSet.ThetaMin = ParseDouble(imageSetElement, XmlNodeType.Attribute, "thetaMin", 0);
					imageSet.ThetaMax = ParseDouble(imageSetElement, XmlNodeType.Attribute, "thetaMax", 0);
					imageSet.PhiMin = ParseDouble(imageSetElement, XmlNodeType.Attribute, "phiMin", 0);
					imageSet.PhiMax = ParseDouble(imageSetElement, XmlNodeType.Attribute, "phiMax", 0);
					double[] rollPitchYaw = ParseThreeDoubleAttribute(imageSetElement, "rollPitchYaw");
					if (rollPitchYaw != null)
					{
						imageSet.Roll = rollPitchYaw[0];
						imageSet.Pitch = rollPitchYaw[1];
						imageSet.Yaw = rollPitchYaw[2];
					}
					imageSet.MaxZoom = ParseDouble(imageSetElement, XmlNodeType.Attribute, "maxZoom", imageSet.MaxZoom);
					imageSet.MaxFieldOfView = ParseDouble(imageSetElement, XmlNodeType.Attribute, "maxFOV", imageSet.MaxFieldOfView);
					imageSet.TileWidth = ParseInt(imageSetElement, XmlNodeType.Attribute, "tileWidth", 256);
					imageSet.TileHeight = ParseInt(imageSetElement, XmlNodeType.Attribute, "tileHeight", 256);
					imageSet.TileOverlap = ParseInt(imageSetElement, XmlNodeType.Attribute, "tileOverlap", 0);
					imageSet.SubRect = ParseFourIntAttribute(imageSetElement, "subRect");
					imageSet.ColorProfile = ExtractValue(imageSetElement, XmlNodeType.Attribute, "colorProfile");
					imageSet.AreTilesPadded = ParseBool(imageSetElement, XmlNodeType.Attribute, "areTilesPadded", false);
					imageSet.IsLowerLeftOrigin = ParseBool(imageSetElement, XmlNodeType.Attribute, "isLowerLeftOrigin", false);

					this.ImageSet = imageSet;
					return true;
				}
			}
			catch
			{
				this.ErrorMessage = "Unable to read HD View XML file.";
			}
			return false;
		}

		private bool ParseZoomifyXml(XDocument document, Uri baseUri)
		{
			try
			{
				// Find the first IMAGE_PROPERTIES element in the XML document.
				XElement imagePropertiesElement = document.Descendants("IMAGE_PROPERTIES").FirstOrDefault();
				if (imagePropertiesElement != null)
				{
					// Initialize the image set.
					this.InputFormat = "Zoomify XML";
					HDViewImageSet imageSet = new HDViewImageSet();
					imageSet.BaseUri = baseUri;
					imageSet.UriPattern = new Uri(imageSet.BaseUri, "TileGroup{g}/{l}-{c}-{r}.jpg").OriginalString;
					imageSet.Width = ParseInt(imagePropertiesElement, XmlNodeType.Attribute, "WIDTH", 0);
					imageSet.Height = ParseInt(imagePropertiesElement, XmlNodeType.Attribute, "HEIGHT", 0);
					int tileSize = ParseInt(imagePropertiesElement, XmlNodeType.Attribute, "TILESIZE", 256);
					imageSet.TileWidth = tileSize;
					imageSet.TileHeight = tileSize;

					// Calculate the number of levels.
					int maxDimension = Math.Max(imageSet.Width, imageSet.Height);
					imageSet.Levels = (int)Math.Ceiling(Math.Log(maxDimension / (double)tileSize, 2)) + 1;
					this.ImageSet = imageSet;
					return true;
				}
			}
			catch
			{
				this.ErrorMessage = "Unable to read Zoomify XML file.";
			}
			return false;
		}

		private bool ParseKml(XDocument document, Uri baseUri, string xpath)
		{
			try
			{
				// Look for PhotoOverlay elements in the XML document.
				XNamespace kmlNamespace = document.Root.Name.Namespace;
				IEnumerable<XElement> photoOverlayElements = document.Descendants(kmlNamespace + "PhotoOverlay");
				XElement photoOverlayElement = photoOverlayElements.FirstOrDefault();
				if (photoOverlayElement == null)
				{
					// There are no PhotoOverlay elements.
					return false;
				}

				// Determine whether we're looking for a PhotoOverlay with a particular ID.
				string id = null;
				if (xpath != null)
				{
					// Create a regular expression that matches "//PhotoOverlay[@id='someID']"
					// with single or double quotes around the ID.
					Regex regex = new Regex("^//PhotoOverlay\\[@id=['\"]([^'\"]*)['\"]\\]$");
					Match match = regex.Match(xpath);
					if (match.Success)
					{
						// Extract the ID from the XPath.
						id = match.Groups[1].Value;
						photoOverlayElement = photoOverlayElements.FirstOrDefault(element => (string)element.Attribute("id") == id);
						if (photoOverlayElement == null)
						{
							// The KML contains PhotoOverlays, but not one with the right ID.
							this.ErrorMessage = "Could not find a PhotoOverlay with ID '" + id + "'.";
							return false;
						}
					}
					else
					{
						// We don't do general XPath queries.
						this.ErrorMessage = "The XPath query '" + xpath + "' is not supported.";
						return false;
					}
				}

				// Do any additional processing required for photo overlays.
				this.ProcessKmlPhotoOverlays(document, baseUri, kmlNamespace, photoOverlayElements, photoOverlayElement);

				// Initialize the image set.
				this.InputFormat = "KML";
				HDViewImageSet imageSet = new HDViewImageSet();
				imageSet.BaseUri = baseUri;

				XElement iconElement = photoOverlayElement.Element(kmlNamespace + "Icon");
				string uriPattern = ExtractValue(iconElement, XmlNodeType.Element, kmlNamespace + "href");
				uriPattern = uriPattern.Replace("$[level]", "{l}").Replace("$[x]", "{c}").Replace("$[y]", "{r}");
				imageSet.UriPattern = new Uri(baseUri, uriPattern).OriginalString;

				XElement imagePyramidElement = photoOverlayElement.Element(kmlNamespace + "ImagePyramid");
				int tileSize = ParseInt(imagePyramidElement, XmlNodeType.Element, kmlNamespace + "tileSize", 256);
				imageSet.TileWidth = imageSet.TileHeight = tileSize;
				imageSet.Width = ParseInt(imagePyramidElement, XmlNodeType.Element, kmlNamespace + "maxWidth", 1);
				imageSet.Height = ParseInt(imagePyramidElement, XmlNodeType.Element, kmlNamespace + "maxHeight", 1);
				int maxDimension = Math.Max(imageSet.Width, imageSet.Height);
				imageSet.Levels = (int)Math.Ceiling(Math.Log(maxDimension / (double)tileSize, 2)) + 1;
				imageSet.AreTilesPadded = true;

				string gridOrigin = ExtractValue(imagePyramidElement, XmlNodeType.Element, kmlNamespace + "gridOrigin");
				if (gridOrigin == null || gridOrigin == "lowerLeft")
				{
					imageSet.IsLowerLeftOrigin = true;
				}

				string shapeValue = ExtractValue(photoOverlayElement, XmlNodeType.Element, kmlNamespace + "shape");
				switch (shapeValue)
				{
					case "cylinder":
						imageSet.Projection = HDViewProjection.Cylindrical;
						break;
					case "sphere":
						imageSet.Projection = HDViewProjection.Spherical;
						break;
				}

				XElement viewVolumeElement = photoOverlayElement.Element(kmlNamespace + "ViewVolume");
				if (viewVolumeElement != null)
				{
					double leftFov = ParseDouble(viewVolumeElement, XmlNodeType.Element, kmlNamespace + "leftFov", 0);
					double rightFov = ParseDouble(viewVolumeElement, XmlNodeType.Element, kmlNamespace + "rightFov", 0);
					double topFov = ParseDouble(viewVolumeElement, XmlNodeType.Element, kmlNamespace + "topFov", 0);
					double bottomFov = ParseDouble(viewVolumeElement, XmlNodeType.Element, kmlNamespace + "bottomFov", 0);

					// If the bottom field of view is exactly zero, it's likely a Gigapan KML file.  We center the
					// vertical field of view on the horizon in this case.
					if (bottomFov == 0)
					{
						topFov /= 2;
						bottomFov = -topFov;
					}

					imageSet.ThetaMin = Clamp(180 + leftFov, 0, 360);
					imageSet.ThetaMax = Clamp(180 + rightFov, 0, 360);
					imageSet.PhiMin = Clamp(90 - topFov, 0, 180);
					imageSet.PhiMax = Clamp(90 - bottomFov, 0, 180);
				}

				this.ImageSet = imageSet;
				return true;
			}
			catch
			{
				this.ErrorMessage = "Unable to read KML file.";
			}
			return false;
		}

		/// <summary>
		/// Extracts a boolean value from the specified attribute or child element of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="nodeType">Either XmlNodeType.Attribute or XmlNodeType.Element.</param>
		/// <param name="attributeOrElementName">The name of the attribute or child element to extract.</param>
		/// <param name="defaultValue">The default value to use if the attribute or child element is absent.</param>
		/// <returns>A boolean value extracted from the specified attribute or element.</returns>
		protected static bool ParseBool(XElement element, XmlNodeType nodeType, XName attributeOrElementName, bool defaultValue)
		{
			string value = ExtractValue(element, nodeType, attributeOrElementName);
			return value == null ? defaultValue : Boolean.Parse(value);
		}

		/// <summary>
		/// Extracts an integer from the specified attribute or child element of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="nodeType">Either XmlNodeType.Attribute or XmlNodeType.Element.</param>
		/// <param name="attributeName">The name of the attribute or child element to extract.</param>
		/// <param name="defaultValue">The default value to use if the attribute or child element is absent.</param>
		/// <returns>An integer extracted from the specified attribute or element.</returns>
		protected static int ParseInt(XElement element, XmlNodeType nodeType, XName attributeOrElementName, int defaultValue)
		{
			string value = ExtractValue(element, nodeType, attributeOrElementName);
			return value == null ? defaultValue : Int32.Parse(value, CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Extracts an enum from the specified attribute of the given element.
		/// </summary>
		/// <typeparam name="T">The type of enum to extract.</typeparam>
		/// <param name="element">The element to process.</param>
		/// <param name="attributeName">The attribute to extract.</param>
		/// <returns>An enum extracted from the specified attribute of the element.</returns>
		protected static T ParseEnumAttribute<T>(XElement element, string attributeName) where T : struct
		{
			XAttribute attribute = element.Attribute(attributeName);
			return attribute == null ? default(T) : (T)Enum.Parse(typeof(T), attribute.Value, true);
		}

		/// <summary>
		/// Extracts a double from the specified attribute or child element of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="nodeType">Either XmlNodeType.Attribute or XmlNodeType.Element.</param>
		/// <param name="attributeOrElementName">The name of the attribute or child element to extract.</param>
		/// <param name="defaultValue">The default value to use if the attribute or child element is absent.</param>
		/// <returns>A double extracted from the specified attribute or element.</returns>
		protected static double ParseDouble(XElement element, XmlNodeType nodeType, XName attributeOrElementName, double defaultValue)
		{
			string value = ExtractValue(element, nodeType, attributeOrElementName);
			return value == null ? defaultValue : Double.Parse(value, CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Extracts the string value from the specified attribute or child element of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="nodeType">Either XmlNodeType.Attribute or XmlNodeType.Element.</param>
		/// <param name="attributeOrElementName">The name of the attribute or child element to extract.</param>
		/// <returns>A string extracted from the specified attribute or element, or null.</returns>
		protected static string ExtractValue(XElement element, XmlNodeType nodeType, XName attributeOrElementName)
		{
			if (nodeType == XmlNodeType.Attribute)
			{
				XAttribute attribute = element.Attribute(attributeOrElementName);
				return attribute == null ? null : attribute.Value;
			}
			else
			{
				XElement childElement = element.Element(attributeOrElementName);
				return childElement == null ? null : childElement.Value;
			}
		}

		/// <summary>
		/// Extracts three doubles from the specified attribute of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="attributeName">The attribute to extract.</param>
		protected static double[] ParseThreeDoubleAttribute(XElement element, string attributeName)
		{
			XAttribute attribute = element.Attribute(attributeName);
			if (attribute == null)
			{
				return null;
			}
			else
			{
				string[] values = attribute.Value.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				double[] result = new double[3];
				result[0] = values.Length <= 0 ? 0 : Double.Parse(values[0], CultureInfo.InvariantCulture);
				result[1] = values.Length <= 1 ? 0 : Double.Parse(values[1], CultureInfo.InvariantCulture);
				result[2] = values.Length <= 2 ? 0 : Double.Parse(values[2], CultureInfo.InvariantCulture);
				return result;
			}
		}

		/// <summary>
		/// Extracts four integers from the specified attribute of the given element.
		/// </summary>
		/// <param name="element">The element to process.</param>
		/// <param name="attributeName">The attribute to extract.</param>
		/// <returns>An array of four integers, or null.</returns>
		protected static int[] ParseFourIntAttribute(XElement element, string attributeName)
		{
			XAttribute attribute = element.Attribute(attributeName);
			if (attribute == null)
			{
				return null;
			}
			else
			{
				string[] values = attribute.Value.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				int[] result = new int[4];
				result[0] = values.Length <= 0 ? 0 : Int32.Parse(values[0], CultureInfo.InvariantCulture);
				result[1] = values.Length <= 1 ? 0 : Int32.Parse(values[1], CultureInfo.InvariantCulture);
				result[2] = values.Length <= 2 ? 0 : Int32.Parse(values[2], CultureInfo.InvariantCulture);
				result[3] = values.Length <= 3 ? 0 : Int32.Parse(values[3], CultureInfo.InvariantCulture);
				return result;
			}
		}

		protected static double Clamp(double value, double min, double max)
		{
			return Math.Max(min, Math.Min(value, max));
		}
	}
}
